とある古典的SQLおじさんのBigQuery入門:ARRAY型とSTRUCT型
本エントリは、クラスメソッド BigQuery Advent Calendar 2020 の 17 日目のエントリです。
25日のアドベントカレンダー終了まで、弊社クラスメソッド データアナリティクス事業本部のメンバーで、Google BigQuery に関する記事を紡いでいこうと思います。
多くのリレーショナルデータベース(RDB)およびRDBをベースにしたデータウェアハウスにおいて、
- データはテーブルに格納する
- テーブルはカラム(列)とレコード(行)で構成される
- カラムとレコードが交わるフィールドには、1つのデータが格納される
- 各カラムは固定の型を持ち、1つのカラムに異なる型のデータを格納できない
などの原則があることをご存知の方は多いと思います。
BigQueryには、標準SQLのデータ型として、INTやVARCHARに加えてARRAYやSTRUCTなど、複雑な構造のデータを格納する型が提供されています。今まであまり馴染みのなかったこれらデータ型について理解を深めます。
ARRAY型とは
ARRAY型とは、ゼロ個以上の同じデータ型の値で構成された順序付きリストのことです。日本語では「配列」といいます。
例えば、以下のゲーム機の名前をARRAY型に格納してみます。
SELECT ['Xbox One', 'Xbox Series X', 'Xbox Series S'] AS consoles;
実行結果は以下です。ご覧のように1行の3つのデータが格納されている事が確認できます。
ちなみにBigQueryの標準SQLにおいては、WITH句を用いて名前付きサブクエリを作成できるようになっています。WITH句で指定したサブクエリの結果が、あたかも実テーブルやビューであるかのように扱えます。
参考:標準SQLへの移行|WITH句を使用したコンポーザビリティ
WITH consoles AS ( SELECT ['Xbox One', 'Xbox Series X', 'Xbox Series S'] AS console_name ) SELECT * FROM consoles;
また、OFFSET()やORDINAL()などの関数を使って、配列内のそれぞれの要素に対して操作することが可能です。
WITH consoles AS ( SELECT ['Xbox One', 'Xbox Series X', 'Xbox Series S'] AS console_name ) SELECT console_name[OFFSET(2)], --0オリジンで数える(=3つ目の要素) console_name[ORDINAL(2)] --1オリジンで数える(=2つ目の要素) FROM consoles;
STRUCT型とは
STRUCT型とは、1つもしくは複数の値をまとめて格納できる構造のことです。日本語では「構造体」といいます。
ゲーム機を提供しているベンダーとゲーム機の情報をSTRUCT型で格納します。
WITH vendors AS ( SELECT STRUCT( 'Microsoft' AS vendor_name, 'Xbox Series X' AS console_name ) AS vendor_set ) SELECT * FROM vendors;
STRUCT型の中にARRAY型を包含する事が可能です。ベンダー1社につきゲーム機は複数提供されているので、ベンダーごとのゲーム機をARRAY型に格納します。
WITH vendor AS ( SELECT STRUCT( 'Microsoft' AS name, ['Xbox One', 'Xbox Series X', 'Xbox Series S'] AS consoles ) AS vendor_set ) SELECT * FROM vendor;
このようにして、複数のベンダーと、ベンダーが提供するゲーム機の情報を、1つのテーブルに格納する事が可能です。 3件のベンダー、ベンダーに紐づく9件のゲーム機のデータを3行のデータとして格納しています。
WITH vendors AS ( SELECT STRUCT( 'Microsoft' AS name, ['Xbox One', 'Xbox Series X', 'Xbox Series S'] AS consoles ) AS vendor_set UNION ALL SELECT STRUCT( 'Nintendo' AS name, ['Wii U', 'Nintendo Switch', 'Nintendo Switch Lite'] AS consoles ) AS vendor_set UNION ALL SELECT STRUCT( 'Sony' AS name, ['Playstation 3', 'Playstation 4', 'Playstation 5'] AS consoles ) AS vendor_set ) SELECT * FROM vendors;
普通の…普通のテーブルが欲しい!
ここまでで、BigQueryはかなり自由な構造でデータを格納する事ができるということが理解できましたが、1つのフィールドに複数のデータが含まれているのは、古典的なSQLおじさんとしてはやっぱり気持ち悪い。
そこで、UNNEST()関数を用いて配列をバラし、9行のデータに展開します。
WITH vendors AS ( SELECT STRUCT( 'Microsoft' AS name, ['Xbox One', 'Xbox Series X', 'Xbox Series S'] AS consoles ) AS vendor_set UNION ALL SELECT STRUCT( 'Nintendo' AS name, ['Wii U', 'Nintendo Switch', 'Nintendo Switch Lite'] AS consoles ) AS vendor_set UNION ALL SELECT STRUCT( 'Sony' AS name, ['Playstation 3', 'Playstation 4', 'Playstation 5'] AS consoles ) AS vendor_set ) SELECT vendor_set.name, unnest_console FROM vendors, UNNEST(vendor_set.consoles) AS unnest_console;
おまけ:FROM句における複数テーブル指定の挙動
SELECT文で使用するFROM句の後にテーブルやビュー名を記述するのは基本中の基本ですが、
BigQueryにおいてテーブルやビュー名をカンマ区切りで羅列するとUNIONと同じ扱いとなります。
しかし、他のRDBMSではFULL OUTER JOIN扱いになる場合があるので気をつけましょう。